/* Display.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Display class.
*/

#include "Display.h"

#include "_sdl/EmbeddedFont.h"

#include <sstream>
#include <iostream>

using std::ostringstream;
using std::cerr;
using std::endl;

// Constants controlling the display dimensions.
const int Display::CELL_WIDTH = 29;
const int Display::CELL_HEIGHT = 20;
const int Display::MARGIN_WIDTH = 64;

const int Display::STATUS_BAR_HEIGHT = 16;

const int Display::SCROLL_BAR_WIDTH = 12;
const int Display::SCROLL_PAD = 2;
const int Display::MIN_SCROLL_BOX = 8;

const int Display::WHEEL_SCROLL = 4;

const int Display::PAD_X = 1;
const int Display::PAD_Y = 3;
const int Display::FILL_WIDTH = Display::CELL_WIDTH - 2 * Display::PAD_X;
const int Display::FILL_HEIGHT = Display::CELL_HEIGHT - 2 * Display::PAD_Y;

const int Display::VIEW_WIDTH = Display::MARGIN_WIDTH + Editor::COLUMNS * Display::CELL_WIDTH + Display::MARGIN_WIDTH;
const int Display::WINDOW_WIDTH = Display::VIEW_WIDTH + Display::SCROLL_BAR_WIDTH;
const int Display::VIEW_HEIGHT = Editor::ROWS * Display::CELL_HEIGHT;
const int Display::WINDOW_HEIGHT = Display::VIEW_HEIGHT + Display::STATUS_BAR_HEIGHT;




// Constructor:
// Create a display, loading resources from the given path.
Display::Display()
	: dragArea(DRAG_NONE), labelDigits(0)
{
	Initialize();
}



// Destructor:
// The destructor quits SDL if it has been initialized.
Display::~Display()
{
	SDL_Quit();
}



// Load:
// Load the file from the given path.
void Display::Load(const string &filePath)
{
	editor.Load(filePath);

	// Figure out how many digits to use in drawing the labels.
	labelDigits = 0;
	size_t size = editor.GetDocument().Size();
	while(size)
	{
		++labelDigits;
		size >>= 4;
	}
}



// Run:
// Run the event loop until the user quits.
void Display::Run()
{
	if(!screen)
		return;
	
	screen.SetCaption("Hexie: " + editor.GetDocument().FileName());
	while(!editor.IsDone())
	{
		Draw();

		SDL_Event event;
		SDL_WaitEvent(&event);

		if(event.type == SDL_QUIT)
		{
			// If the user closes the window, save the document.
			editor.GetDocument().Save();
			break;
		}
		else if(event.type == SDL_KEYDOWN)
			editor.Handle(event.key.keysym.sym, event.key.keysym.mod);
		else if(event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONUP)
			HandleMouse(event);
	}
}



// Initialize:
// Create the window and load the resources.
void Display::Initialize()
{
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		cerr << "Unable to initialize SDL." << endl;
		return;
	}

	font.Load(EmbeddedFont::DATA, EmbeddedFont::WIDTH, EmbeddedFont::HEIGHT);

	SDL_SetEventFilter(&EventFilter);
	SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
	
	screen.SetCaption("Hexie");
	// Attempt to create the window. If successful, we are fully initialized.
	screen.Create(WINDOW_WIDTH, WINDOW_HEIGHT);

	// TODO: store this in a preferences file.
	typeColor[0] = screen.Color(255, 255, 255);
	typeColor['H'] = screen.Color(240, 240, 240);
	typeColor['C'] = screen.Color(248, 248, 224);
	typeColor['B'] = screen.Color(224, 224, 224);
	typeColor['I'] = screen.Color(232, 248, 224);
	typeColor['U'] = screen.Color(224, 240, 232);
	typeColor['F'] = screen.Color(224, 224, 248);
	typeColor['X'] = screen.Color(160, 160, 160);
	typeColor['0'] = screen.Color(255, 255, 255);
	typeColor['1'] = screen.Color(0, 0, 0);
	typeColor['_'] = screen.Color(48, 32, 96);
	typeColor['.'] = screen.Color(216, 208, 240);
	typeColor['|'] = screen.Color(0, 0, 0);
	typeColor['s'] = screen.Color(160, 160, 160);
	typeColor['['] = screen.Color(160, 160, 160);
	typeColor['*'] = screen.Color(64, 64, 64);
}



// HandleMouse:
// Handle the given mouse event.
void Display::HandleMouse(const SDL_Event &event)
{
	bool keepAnchor = true;
	int x = 0;
	int y = 0;
	
	if(event.type == SDL_MOUSEBUTTONDOWN)
	{
		if(event.button.button != 1)
		{
			// Handle the scroll wheel.
			editor.ScrollToRow(editor.ScrollRow() + WHEEL_SCROLL * ((event.button.button == 5) - (event.button.button == 4)));
			return;
		}

		x = event.button.x;
		y = event.button.y;
		SDLMod mod = SDL_GetModState();
		keepAnchor = ((mod & KMOD_SHIFT) != 0);

		// Figure out where in the view the mouse is.
		if(x < VIEW_WIDTH && y < VIEW_HEIGHT)
			dragArea = DRAG_VIEW;
		else if(y < VIEW_HEIGHT)
			dragArea = DRAG_SCROLL;
		else
			dragArea = DRAG_NONE;
	}
	else if(event.type == SDL_MOUSEMOTION)
	{
		x = event.motion.x;
		y = event.motion.y;
	}
	else
		dragArea = DRAG_NONE;
	
	if(dragArea == DRAG_VIEW)
	{
		Editor::Cell cell;
		cell.row = y / CELL_HEIGHT;
		cell.column = (x - MARGIN_WIDTH) / CELL_WIDTH;
		
		editor.PlaceCursor(cell, keepAnchor);
	}
	else if(dragArea == DRAG_SCROLL)
	{
		// Calculate the row corresponding to this click.
		editor.ScrollToRow((y * editor.TotalRows()) / VIEW_HEIGHT - editor.VisibleRows() / 2);
	}
}



// Draw:
// Redraw the entire display.
void Display::Draw()
{
	screen.Erase(typeColor[0]);

	// Draw the cursor first, because this also may update the scroll.
	DrawCursor();

	// Get an iterator to the first markup that extends to beyond the scroll position.
	const map<Document::It, Type> &markup = editor.GetDocument().Markup();
	const Type &defaultType = editor.GetDocument().DefaultType();
	// Start either with the first byte, or the mark that affects the first byte.
	Document::It it = editor.Lines().front();
	map<Document::It, Type>::const_iterator typeIt = editor.GetDocument().MarkupOf(it);
	if(typeIt == markup.end())
		typeIt = markup.upper_bound(it);
	else
		it = typeIt->first;

	// Continue to the last byte visible on the screen.
	string::const_iterator end = editor.Lines().back();

	// Draw each byte. If a mark is set for that byte, draw it using the marked type.
	// If marks overlap, only show the leftmost one.
	while(it < end)
	{
		while(typeIt != markup.end() && typeIt->first < it)
			++typeIt;

		Type type = defaultType;
		if(typeIt != markup.end() && it == typeIt->first)
		{
			type = typeIt->second;
			++typeIt;
		}

		Document::It nextMark = (typeIt == markup.end()) ? editor.GetDocument().End() : typeIt->first;

		string::const_iterator thisEnd = it + type.TotalBytes();
		for( ; it < thisEnd; it += type.ElementSize())
			DrawData(it, nextMark, type);

		if(it > nextMark)
		{
			if(it > editor.GetDocument().End())
				break;
			it = nextMark;
		}
	}

	// Draw the row labels.
	int y = PAD_Y;
	for(vector<Document::It>::const_iterator lineIt = editor.Lines().begin(); lineIt != editor.Lines().end() - 1; ++lineIt)
	{
		font.Draw(HexLabel(*lineIt - editor.GetDocument().Begin()), screen, PAD_X, y);
		y += CELL_HEIGHT;
	}
	
	DrawStatusBar();
	
	DrawScrollBar();
	
	screen.Update();
}



// Draw:
// Draw the given data with the given element type.
// Returns false if the data begins off the bottom of the screen.
void Display::DrawData(Document::It it, Document::It end, const Type &type)
{
	// Make sure we don't go past the end of the document.
	if(end > editor.GetDocument().End())
		end = editor.GetDocument().End();

	// Figure out how many cells will be filled.
	int cells = type.ElementSize();

	// Check that there is enough data left in the file to read.
	if(it + cells > end)
	{
		// If this will fill beyond the end of the file, draw 'X' boxes instead.
		// These boxes may span multiple rows.
		bool isFirst = true;
		for( ; it < end; ++it)
		{
			bool isLast = (it + 1 == end);

			Editor::Cell cell = editor.CellOf(it);
			screen.FillRect(MARGIN_WIDTH + cell.column * CELL_WIDTH + PAD_X * isFirst, cell.row * CELL_HEIGHT + PAD_Y,
				CELL_WIDTH - PAD_X * (isFirst + isLast), FILL_HEIGHT, typeColor['X']);

			isFirst = false;
		}

		return;
	}

	// Find the start and end row and column.
	Editor::Cell startCell = editor.CellOf(it);
	Editor::Cell endCell = editor.CellOf(it + (cells - 1));
	
	if(startCell.row >= editor.VisibleRows() || endCell.row < 0)
		return;
		
	int x = MARGIN_WIDTH + startCell.column * CELL_WIDTH + PAD_X;
	int y = startCell.row * CELL_HEIGHT + PAD_Y;
	
	int blockWidth = cells * CELL_WIDTH - 2 * PAD_X;
	// If the block spans a row, fill in to the end of the first row.
	if(startCell.row < endCell.row)
		blockWidth = VIEW_WIDTH - x;
	
	// Fill the first (possibly the only) row.
	Uint32 backColor = typeColor[type.DataType()];
	screen.FillRect(x, y, blockWidth, FILL_HEIGHT, backColor);
	
	// Assume that single data blocks will span at most two rows.
	// (That is, that COLUMNS >= largest element size).
	int endBlockWidth = 0;
	if(startCell.row < endCell.row)
	{
		endBlockWidth = (endCell.column + 1) * CELL_WIDTH - PAD_X;
		screen.FillRect(MARGIN_WIDTH, y + CELL_HEIGHT, endBlockWidth, FILL_HEIGHT, backColor);
	}
	
	// Draw the data text.
	string text = type.Read(it);
	if(type.DataType() == 'B')
	{
		// Special case: drawing binary data.
		// Assume that the element size is 1, so we're in a single cell.
		// Binary bars are divided by 7 single-pixel lines.
		int barWidth = (CELL_WIDTH - 2 * PAD_X - 9) / 8;
		// Center the binary display in the cell.
		x += (CELL_WIDTH - (8 * barWidth + 7)) / 2 - PAD_X;
		for(int i = 0; i < 8; ++i)
		{
			screen.FillRect(x, y + 1, barWidth, CELL_HEIGHT - 2 * (PAD_Y + 1), typeColor[text[i]]);
			x += barWidth + 1;
		}
	}
	else
	{
		int textWidth = font.Width(text);

		// Center the text within the block.
		// If this block is split into two rows, and if the next row is wider than this one,
		// draw the text on the second row. Otherwise, draw it in the main block.
		if(endBlockWidth > blockWidth)
		{
			y += CELL_HEIGHT;
			x = MARGIN_WIDTH + (endBlockWidth - textWidth) / 2;
		}
		else
			x += (blockWidth - textWidth) / 2;

		font.Draw(text, screen, x, y);
	}
}



// DrawCursor:
void Display::DrawCursor()
{
	// Draw the selection.
	Editor::Cell start = editor.CellOf(editor.GetDocument().SelectionBegin());
	Editor::Cell end = editor.CellOf(editor.GetDocument().SelectionEnd());
	
	// Fill in to the end of every row but the last selected row.
	for( ; start.row < end.row; ++start.row)
	{
		int x = MARGIN_WIDTH + start.column * CELL_WIDTH;
		screen.FillRect(x, start.row * CELL_HEIGHT, VIEW_WIDTH - x, CELL_HEIGHT, typeColor['.']);
		
		start.column = 0;
	}
	screen.FillRect(MARGIN_WIDTH + start.column * CELL_WIDTH, start.row * CELL_HEIGHT,
		(end.column + 1 - start.column) * CELL_WIDTH, CELL_HEIGHT, typeColor['.']);
	
	// Draw the cursor.
	Editor::Cell cursor = editor.CellOf(editor.GetDocument().Cursor());
	screen.FillRect(MARGIN_WIDTH + cursor.column * CELL_WIDTH, cursor.row * CELL_HEIGHT, 
		CELL_WIDTH, CELL_HEIGHT, typeColor['_']);
}



// DrawStatusBar:
// Draw the status bar.
void Display::DrawStatusBar()
{
	int y = WINDOW_HEIGHT - STATUS_BAR_HEIGHT;
	
	screen.HLine(0, y, WINDOW_WIDTH, typeColor['|']);
	screen.FillRect(0, y + 1, WINDOW_WIDTH, STATUS_BAR_HEIGHT - 1, typeColor['s']);
	
	y += 2;
	int x = 2;
	x = font.Draw(Type::EndianString(), screen, x, y);
	x += 10;
	x = font.Draw(editor.GetDocument().DefaultType().TypeString(), screen, x, y);
	x += 10;
	
	// Draw various interpretations of the cursor.
	Document::It cursor = editor.GetDocument().Cursor();
	if(cursor < editor.GetDocument().End())
	{
		string interpret;
		Type type;
		interpret += "0x" + type.Read(cursor);
		
		type.SetDataType('B');
		interpret += " / 0b" + type.Read(cursor);
		
		type.SetDataType('C');
		interpret += " ('" + type.Read(cursor) + "')";

		type.SetDataType('I');
		interpret += " {" + type.Read(cursor);
		
		type.SetDataType('U');
		if(cursor + 1 < editor.GetDocument().End())
		{
			type.SetDataType('S');
			interpret += ", " + type.Read(cursor);
			
			if(cursor + 3 < editor.GetDocument().End())
			{
				type.SetDataType('L');
				interpret += ", " + type.Read(cursor);
			}
		}
		
		interpret += "}";
		
		if(cursor + 3 < editor.GetDocument().End())
		{
			type.SetDataType('F');
			interpret += " [" + type.Read(cursor);
			
			if(cursor + 7 < editor.GetDocument().End())
			{
				type.SetDataType('D');
				interpret += ", " + type.Read(cursor);
			}
			interpret += "]";
		}
		
		x = (WINDOW_WIDTH - font.Width(interpret)) / 2;
		font.Draw(interpret, screen, x, y);
	}
	
	// Draw the cursor position.
	size_t index = cursor - editor.GetDocument().Begin();
	size_t size = editor.GetDocument().Size();
	
	ostringstream out;
	out<<index<<" / "<<size;
	x = WINDOW_WIDTH - (font.Width(out.str()) + PAD_X);
	font.Draw(out.str(), screen, x, y);
}



// DrawScrollBar:
// Draw the scroll bar.
void Display::DrawScrollBar()
{
	screen.FillRect(VIEW_WIDTH, 0, SCROLL_BAR_WIDTH, VIEW_HEIGHT, typeColor['[']);
	int visible = editor.VisibleRows();
	int scroll = editor.ScrollRow();
	int total = editor.TotalRows();

	if(!total)
		return;
	
	int y = (scroll * VIEW_HEIGHT) / total;
	int h = (visible * VIEW_HEIGHT) / total;
	
	if(h < MIN_SCROLL_BOX)
	{
		// The scroll slider can get no smaller than a certain height.
		y -= (MIN_SCROLL_BOX - h) / 2;
		h = MIN_SCROLL_BOX;
	}
	// Make sure the scroll slider is in the allowed area.
	// It can move out if the cursor is on the last line and the last line is only an end of file.
	if(y < 0)
		y = 0;
	else if(y + h > VIEW_HEIGHT)
		y = VIEW_HEIGHT - h;
	
	screen.FillRect(VIEW_WIDTH + SCROLL_PAD, y, SCROLL_BAR_WIDTH - 2 * SCROLL_PAD, h, typeColor['*']);
}



// HexLabel:
// Convert an index into a hexadecimal string.
// Use only as many characters as are needed for this file.
string Display::HexLabel(size_t index) const
{
	string label(labelDigits, '0');

	for(int i = 0; index; ++i)
	{
		char c = static_cast<char>(index & 0xF);
		
		index >>= 4;
		label[(labelDigits - 1) - i] = (c < 10) ? (c + '0') : ((c - 10) + 'a');
	}

	return label;
}



// EventFilter:
// This function filters events that we don't care about.
int Display::EventFilter(const SDL_Event *event)
{
	// Report quit events, key events, mouse clicks, and left mouse drags.
	// For clicks, we only care about buttons 1 down or up, or button 4 or 5 down.
	// (Buttons 4 and 5 are scroll wheel up and down.)
	return (event->type == SDL_QUIT || event->type == SDL_KEYDOWN || 
		(event->type == SDL_MOUSEBUTTONDOWN && (event->button.button == 1 || 
			event->button.button == 4 || event->button.button == 5)) ||
		(event->type == SDL_MOUSEBUTTONUP && event->button.button == 1) || 
		(event->type == SDL_MOUSEMOTION && (event->motion.state & SDL_BUTTON(1)) != 0));
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
